// SPDX-License-Identifier: GPL-2.0
/* Copyright(c) 2021 3snic Technologies Co., Ltd */

#define pr_fmt(fmt) KBUILD_MODNAME ": [IPsec]" fmt

#include <linux/inetdevice.h>
#include <linux/netdevice.h>
#include <linux/module.h>
#include <net/xfrm.h>
#include <net/esp.h>
#include <crypto/aead.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/etherdevice.h>
#include <linux/if_vlan.h>
#include <linux/ethtool.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/pci.h>
#include <linux/netdevice.h>
#include <linux/module.h>

#include "ossl_knl.h"
#include "sssnic_hw.h"
#include "sssnic_crm.h"
#include "sssnic_dev.h"
#include "sssnic_txrx.h"

#include "sssnic_ipsec.h"
#include "sssnic_cqm.h"
#include "sssnic_common.h"
#include "sssnic_msg.h"



unsigned char ipsec_work_mode = SSSEC_IPSEC_OFFLOAD_MODE_DISABLE;
module_param(ipsec_work_mode, byte, 0444);
MODULE_PARM_DESC(ipsec_work_mode, "IPsec work mode:(0-disable 1-xfrm offload 2-hwacc)");


#define SSSNIC_IPSEC_STATS_INC(ipsec, field)		\
do {							\
	u64_stats_update_begin(&(ipsec)->stats.syncp);	\
	(ipsec)->stats.field++;				\
	u64_stats_update_end(&(ipsec)->stats.syncp);	\
} while (0)



int ssscec_send_msg_to_mpu_sync(void *hwdev, u8 cmd, void *buf_in, u16 in_size, void *buf_out, u16 *out_size)
{
    /* todo:: vf need to mpu com or not ? */
    return ssshw_msg_to_mgmt_sync(hwdev, SSSHW_MOD_CRYPT, cmd, buf_in, in_size, buf_out, out_size, 0,
        SSSHW_CHANNEL_CRYPT);
}

int ssscec_reg_cqm_service(void *hwdev, struct ssshw_tag_service_register_template *reg_temp)
{
    if (!hwdev || (reg_temp->service_type >= SSSHW_SERVICE_T_MAX))
        return -EINVAL;

    return ssshw_cqm_service_register(hwdev, reg_temp);
}

void ssscec_unreg_cqm_service(void *hwdev, u32 service_type)
{
    if (!hwdev || (service_type >= SSSHW_SERVICE_T_MAX))
        return;

    ssshw_cqm_service_unregister(hwdev, service_type);
}

int ssscec_set_ipsec_offload_mode(void *hwdev, u8 offload_mode)
{
    struct sssnic_nic_io *nic_io = NULL;
    struct sssec_cmd_ipsec_offload_mode ipsec_cfg;
    u16 out_size = sizeof(ipsec_cfg);
    int err;

    if (!hwdev)
        return -EINVAL;

    nic_io = sssnic_get_service_adapter(hwdev, SSSHW_SERVICE_T_NIC);

    memset(&ipsec_cfg, 0, sizeof(ipsec_cfg));
    ipsec_cfg.func_id = ssshw_global_func_id(hwdev);
    ipsec_cfg.offload_mode = offload_mode;

    err = ssscec_send_msg_to_mpu_sync(hwdev, SSSEC_MPU_CMD_SET_IPSEC_OFFLOAD_MODE, &ipsec_cfg, sizeof(ipsec_cfg),
        &ipsec_cfg, &out_size);
    if (err || !out_size || ipsec_cfg.msg_head.status) {
        nic_err(nic_io->dev_hdl, "Failed to set ipsec offload mode, err: %d, status: 0x%x, out size: 0x%x\n", err,
            ipsec_cfg.msg_head.status, out_size);
        return -EINVAL;
    }

    return 0;
}

int ssscec_get_crypto_stats(void *hwdev, struct sssec_crypto_stats *stats)
{
    struct sssec_cmd_cryptodev_stats cdev_stats;
    u16 out_size = sizeof(cdev_stats);
    struct sssnic_nic_io *nic_io = NULL;
    int err;

    if (!hwdev || !stats)
        return -EINVAL;

    nic_io = sssnic_get_service_adapter(hwdev, SSSHW_SERVICE_T_NIC);
    memset(&cdev_stats, 0, sizeof(cdev_stats));

    err = ssscec_send_msg_to_mpu_sync(hwdev, SSSEC_MPU_CMD_GET_CRYPTO_STATS,
                                     &cdev_stats, sizeof(cdev_stats),
                                     &cdev_stats, &out_size);
    if (err || !out_size || cdev_stats.msg_head.status) {
        nic_err(nic_io->dev_hdl,
                "Failed to get cryptodev statistics, err: %d, status: 0x%x, out size: 0x%x\n",
                err, cdev_stats.msg_head.status, out_size);
        return -EFAULT;
    }

    memcpy(stats, &cdev_stats.stats, sizeof(*stats));

    return 0;
}

int ssscec_set_ipsec_policy(void *hwdev, sssec_cmd_set_ipsec_sp_s *sp_msg)
{
    struct sssnic_nic_io *nic_io = NULL;
    struct ssshw_cmd_buf *cmd_buf = NULL;
    sssec_cmd_set_ipsec_sp_s *sp_msg_buf = NULL;
    int ret = 0;
    u64 out_param = 0;

    if (!hwdev || !sp_msg)
        return -EINVAL;

    nic_io = sssnic_get_service_adapter(hwdev, SSSHW_SERVICE_T_NIC);

    cmd_buf = sssnic_alloc_cmd_buf(hwdev);
    if (!cmd_buf) {
        sdk_err(nic_io->dev_hdl, "Failed to alloc cmd buf\n");
        return -ENOMEM;
    }

    cmd_buf->size = sizeof(*sp_msg_buf);
    sp_msg_buf = (sssec_cmd_set_ipsec_sp_s *)cmd_buf->buf;
    memcpy(sp_msg_buf, sp_msg, sizeof(sssec_cmd_set_ipsec_sp_s));

    ret = ssshw_cmdq_direct_resp(hwdev, SSSHW_MOD_CRYPT, SSSEC_NPU_CMD_SET_IPSEC_SP, cmd_buf,
        &out_param, 0, SSSHW_CHANNEL_CRYPT);
    if (ret || out_param) {
        sdk_err(nic_io->dev_hdl, "Failed to set IPsec policy, ret: %d, out_param: 0x%llx\n", ret, out_param);
        ret = -EFAULT;
    }

    ssshw_free_cmd_buf(hwdev, cmd_buf);

    return ret;
}

int ssscec_set_ipsec_sa(void *hwdev, sssec_cmd_set_ipsec_sa_s *sa_msg)
{
    struct sssnic_nic_io *nic_io = NULL;
    struct ssshw_cmd_buf *cmd_buf = NULL;
    sssec_cmd_set_ipsec_sa_s *sa_msg_buf = NULL;
    int ret = 0;
    u64 out_param = 0;

    if (!hwdev || !sa_msg)
        return -EINVAL;

    nic_io = sssnic_get_service_adapter(hwdev, SSSHW_SERVICE_T_NIC);

    cmd_buf = sssnic_alloc_cmd_buf(hwdev);
    if (!cmd_buf) {
        sdk_err(nic_io->dev_hdl, "Failed to alloc cmd buf\n");
        return -ENOMEM;
    }

    cmd_buf->size = sizeof(*sa_msg_buf);
    sa_msg_buf = (sssec_cmd_set_ipsec_sa_s *)cmd_buf->buf;
    memcpy(sa_msg_buf, sa_msg, sizeof(*sa_msg));

    ret = ssshw_cmdq_direct_resp(hwdev, SSSHW_MOD_CRYPT,
    				  SSSEC_NPU_CMD_SET_IPSEC_SA, cmd_buf,
        			  &out_param, 0, SSSHW_CHANNEL_CRYPT);
    if (ret || out_param) {
        sdk_err(nic_io->dev_hdl, "Failed to set IPsec SA context, ret: %d, out_param: 0x%llx\n",
		ret, out_param);
        ret = -EFAULT;
    }

    ssshw_free_cmd_buf(hwdev, cmd_buf);

    return ret;
}

int ssscec_flush_ipsec_resource(void *hwdev, u32 flush_flag)
{
    struct sssnic_nic_io *nic_io = NULL;
    struct ssshw_cmd_buf *cmd_buf = NULL;
    sssec_cmd_flush_ipsec_res_s *flush_msg_buf = NULL;
    int ret = 0;
    u64 out_param = 0;

    if (!hwdev)
        return -EINVAL;

    nic_io = sssnic_get_service_adapter(hwdev, SSSHW_SERVICE_T_NIC);

    cmd_buf = sssnic_alloc_cmd_buf(hwdev);
    if (!cmd_buf) {
        sdk_err(nic_io->dev_hdl, "Failed to alloc cmd buf\n");
        return -ENOMEM;
    }

    cmd_buf->size = sizeof(*flush_msg_buf);
    flush_msg_buf = (sssec_cmd_flush_ipsec_res_s *)cmd_buf->buf;
    flush_msg_buf->func_id = cpu_to_be16(ssshw_global_func_id(hwdev));
    flush_msg_buf->flush_flag = cpu_to_be32(flush_flag);
    flush_msg_buf->cmdhdr.dw0.bs.cmd_type = SSSEC_NPU_CMD_FLUSH_IPSEC_RES;
    flush_msg_buf->cmdhdr.dw0.value = cpu_to_be32(flush_msg_buf->cmdhdr.dw0.value);

    ret = ssshw_cmdq_direct_resp(hwdev, SSSHW_MOD_CRYPT,
    				  SSSEC_NPU_CMD_FLUSH_IPSEC_RES, cmd_buf,
    				  &out_param, 0, SSSHW_CHANNEL_CRYPT);
    if (ret || out_param) {
        sdk_err(nic_io->dev_hdl, "Failed to flush IPsec res, ret: %d, out_param: 0x%llx\n",
		ret, out_param);
        ret = -EFAULT;
    }

    ssshw_free_cmd_buf(hwdev, cmd_buf);

    return ret;
}

static void sssnic_ipsec_clean_stats(struct sssnic_ipsec_stats *ipsec_stats)
{
	u64_stats_update_begin(&ipsec_stats->syncp);
	memset(ipsec_stats, 0,
	       sizeof(*ipsec_stats) - sizeof(ipsec_stats->syncp));
	u64_stats_update_end(&ipsec_stats->syncp);
}

static void sssnic_ipsec_init_stats(struct sssnic_ipsec *ipsec)
{
	struct sssnic_ipsec_stats *ipsec_stats = &ipsec->stats;

	u64_stats_init(&ipsec_stats->syncp);
	sssnic_ipsec_clean_stats(ipsec_stats);
}

int sssnic_init_ipsec_offload(struct sssnic_nic_dev *nic_dev)
{
	struct sssnic_ipsec *ipsec = NULL;
	struct sssnic_ipsec_xmap *xmap = NULL;
	u32 xmap_size;
	int ret;
	u16 glb_func_id;

	if (!ssshw_support_ipsec(nic_dev->hwdev, NULL)) {
		nic_dev->ipsec = NULL;
		nic_info(&nic_dev->pdev->dev, "Hw don't support ipsec\n");
		return 0;
	}

	ret = ssshw_stateful_init(nic_dev->hwdev);
	if (ret) {
		nic_info(&nic_dev->pdev->dev, "init statefull failed");
		return ret;
	}

	glb_func_id = ssshw_global_func_id(nic_dev->hwdev);
	ret = ssshw_func_reset(nic_dev->hwdev, glb_func_id, SSSHW_IPSEC_RES,
				SSSHW_CHANNEL_CRYPT);
	if (ret) {
		nic_err(&nic_dev->pdev->dev, "Failed to reset IPsec function:%d\n",
			ret);
		goto func_reset_fail;
	}

	ipsec = kzalloc(sizeof(*ipsec), GFP_KERNEL);
	if (!ipsec) {
		ret = -ENOMEM;
		nic_err(&nic_dev->pdev->dev, "Alloc ipsec mem failed\n");
		goto alloc_ipsec_fail;
	}

	/* xid 0 & 1 internal used by CQM */
	xmap_size = sizeof(*xmap) * (SSSNIC_IPSEC_PER_PF_SA_CTX_NUM +
				     SSSNIC_IPSEC_XID_RESERVE_NUM);
	xmap = kzalloc(xmap_size, GFP_KERNEL);
	if (!xmap) {
		ret = -ENOMEM;
		nic_err(&nic_dev->pdev->dev, "Alloc xmap mem failed\n");
		goto alloc_xmap_fail;
	}
	ipsec->xmap = xmap;

	hash_init(ipsec->sadb);
	spin_lock_init(&ipsec->sadb_lock);

	ipsec->nic_dev = nic_dev;
	nic_dev->ipsec = ipsec;
	ipsec->no_trailer = true; /* ESP trailer always removed by hw */

	sssnic_ipsec_init_stats(ipsec);

	ret = ssscec_set_ipsec_offload_mode(nic_dev->hwdev, ipsec_work_mode);
	if (ret) {
		nic_err(&nic_dev->pdev->dev,
			"Set IPsec offload mode failed: %d\n", ret);
		goto set_mode_fail;
	}

	nic_info(&nic_dev->pdev->dev,
		 "Init IPsec offload mode(0x%x) success\n", ipsec_work_mode);

	return ret;

set_mode_fail:
	kfree(ipsec->xmap);
alloc_xmap_fail:
	kfree(ipsec);
	nic_dev->ipsec = NULL;
alloc_ipsec_fail:
func_reset_fail:
	ssshw_stateful_deinit(nic_dev->hwdev);
	return ret;
}

void sssnic_cleanup_ipsec_offload(struct sssnic_nic_dev *nic_dev)
{
	struct sssnic_ipsec *ipsec = nic_dev->ipsec;
	int ret;

	if (!ssshw_support_ipsec(nic_dev->hwdev, NULL))
		return;

	if (!ipsec) {
		nic_err(&nic_dev->pdev->dev, "Param ipsec is NULL\n");
		return;
	}

	ret = ssscec_set_ipsec_offload_mode(nic_dev->hwdev, 0);
	if (ret) {
		nic_err(&nic_dev->pdev->dev,
			"Set IPsec offload mode failed: %d\n", ret);
	}

	ret = ssscec_flush_ipsec_resource(nic_dev->hwdev, SSSEC_IPSECRES_ALL);
	if (ret) {
		nic_err(&nic_dev->pdev->dev, "Failed to flush IPsec res:%d\n",
			ret);
	}

	kfree(ipsec->xmap);
	kfree(ipsec);
	nic_dev->ipsec = NULL;

	ssshw_stateful_deinit(nic_dev->hwdev);

	nic_info(&nic_dev->pdev->dev, "Cleanup IPsec offload success\n");
}
